/*
 * NumberValue.java
 */

package complexchaos;

import java.util.*;
import java.math.*;
import java.lang.Math;
import java.lang.*;

/**
 * A class to store numbers. It can contain them either as ratios (n/d) or as
 * exact values.
 *
 * @author Don Spickler
 */
public class NumberValue implements Cloneable 
{
	private boolean exact;
	private BigInteger num = null;
	private BigInteger den = null;
	private double val;              //  Maybe change to a BigDecimal?
	
	public NumberValue(final String n, final String d)
	{
		exact = true;
		num = new BigInteger(n);
		den = new BigInteger(d);
		val = 0;
		reduce();
	}
	
	public NumberValue(final long n, final long d)
	{
		exact = true;
		num = new BigInteger(""+n);
		den = new BigInteger(""+d);
		val = 0;
		reduce();
	}

	public NumberValue(final String v)
	{
		exact = false;
		num = null;
		den = null;
		val = Double.valueOf(v);
	}

	public NumberValue(final BigInteger n, final BigInteger d)
	{
		exact = true;
		num = new BigInteger(n.toString());
		den = new BigInteger(d.toString());
		val = 0;
		reduce();
	}
	
	public NumberValue(final double v)
	{
		exact = false;
		num = null;
		den = null;
		val = v;
	}

	public boolean isExact()
	{
		return exact;
	}
	
	public static NumberValue makeZero()
	{
		return new NumberValue("0", "1");
	}

	public static NumberValue makeOne()
	{
		return new NumberValue("1", "1");
	}

	public static NumberValue makeMinusOne()
	{
		return new NumberValue("-1", "1");
	}

	public boolean isPos()
	{
		boolean retval = false;
		
		if (isExact())
		{
			reduce();
			if (num.compareTo(BigInteger.ZERO) > 0)
				retval = true;
		}
		else
		{
			retval = (val > 0);
		}
		
		return retval;
	}

	public boolean isNeg()
	{
		return !isPos();
	}

	public boolean isZero()
	{
		if (exact)
		{
			reduce();
			if ((num.compareTo(BigInteger.ZERO) == 0) && (den.compareTo(BigInteger.ONE) == 0))
				return true;
		}
		else
			if (val == 0)
				return true;

		return false;
	}

	public boolean isZero(NumericTolerances tol)
	{
		if (exact)
		{
			reduce();
			if ((num.compareTo(BigInteger.ZERO) == 0) && (den.compareTo(BigInteger.ONE) == 0))
				return true;
		}
		else
		{
			if (tol.getUseZeroTolerance())
			{
				if (Math.abs(val) < tol.getZeroTolerance())
					return true;
			}
			else
			{
				if (val == 0)
					return true;
			}
		}
		return false;
	}

	public boolean isOne()
	{
		if (exact)
		{
			reduce();			
			if ((num.compareTo(BigInteger.ONE) == 0) && (den.compareTo(BigInteger.ONE) == 0))
				return true;
		}
		else
			if (val == 1)
				return true;

		return false;
	}

	public boolean isOne(NumericTolerances tol)
	{
		if (exact)
		{
			reduce();			
			if ((num.compareTo(BigInteger.ONE) == 0) && (den.compareTo(BigInteger.ONE) == 0))
				return true;
		}
		else
		{
			if (tol.getUseEqualityTolerance())
			{
				if (Math.abs(val - 1) < tol.getEqualityTolerance())
					return true;
			}
			else
			{
				if (val == 1)
					return true;
			}			
		}
		return false;
	}

	public boolean isMinusOne()
	{
		if (exact)
		{
			reduce();			
			if ((num.compareTo(BigInteger.ONE.negate()) == 0) && (den.compareTo(BigInteger.ONE) == 0))
				return true;
		}
		else
			if (val == -1)
				return true;

		return false;
	}

	public boolean isMinusOne(NumericTolerances tol)
	{
		if (exact)
		{
			reduce();			
			if ((num.compareTo(BigInteger.ONE.negate()) == 0) && (den.compareTo(BigInteger.ONE) == 0))
				return true;
		}
		else
		{
			if (tol.getUseEqualityTolerance())
			{
				if (Math.abs(val + 1) < tol.getEqualityTolerance())
					return true;
			}
			else
			{
				if (val == -1)
					return true;
			}			
		}
		return false;
	}

	public boolean isInteger()
	{
		if (exact)
		{
			reduce();
			if (den.compareTo(BigInteger.ONE) == 0)
				return true;
		}
		return false;
	}

	public NumberValue AdjustByTolerances(NumericTolerances tol)
	{
		NumberValue retval = this.clone();
		if (retval.isZero(tol))
			retval = NumberValue.makeZero();
		if (retval.isOne(tol))
			retval = NumberValue.makeOne();
		if (retval.isMinusOne(tol))
			retval = NumberValue.makeMinusOne();
			
		return retval;
	}

	public NumberValue AdjustToInteger(NumericTolerances tol)
	{
		NumberValue retval = this.clone();
		
		if (retval.isExact()) return retval;
		
		try
		{
			double apval = approx();
			long roundval = Math.round(apval);
			if (Math.abs(apval - roundval) < tol.getEqualityTolerance())
				return new NumberValue(roundval, 1);
		}
		catch (Exception ex) { }
					
		return retval;
	}
		
	public NumberValue add(NumberValue n)
	{
		NumberValue retval = null;
		
		try
		{
			if (exact && n.isExact())
			{
				retval = new NumberValue((num.multiply(n.getDen())).add(den.multiply(n.getNum())), den.multiply(n.getDen()));
				retval.reduce();
			}
			else
				retval = new NumberValue(approx() + n.approx());
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: add method numeric exception.");
		}

		return retval;
	}

	public NumberValue subtract(NumberValue n)
	{
		NumberValue retval = null;
		
		try
		{
			if (exact && n.isExact())
			{
				retval = new NumberValue((num.multiply(n.getDen())).subtract(den.multiply(n.getNum())), den.multiply(n.getDen()));
				retval.reduce();
			}
			else
				retval = new NumberValue(approx() - n.approx());
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: subtract method numeric exception.");
		}

		return retval;
	}
	
	public NumberValue multiply(NumberValue n)
	{
		NumberValue retval = null;
		
		try
		{
			if (exact && n.isExact())
			{
				retval = new NumberValue(num.multiply(n.getNum()), den.multiply(n.getDen()));
				retval.reduce();
			}
			else
				retval = new NumberValue(approx() * n.approx());
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: multiply method numeric exception.");
		}

		return retval;
	}
	
	public NumberValue divide(NumberValue n)
	{
		NumberValue retval = null;
		
		try
		{
			if (exact && n.isExact())
			{
				retval = new NumberValue(num.multiply(n.getDen()), den.multiply(n.getNum()));
				retval.reduce();
			}
			else
				retval = new NumberValue(approx() / n.approx());
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: divide method numeric exception.");
		}

		return retval;
	}

	public NumberValue negate()
	{
		NumberValue retval = null;
		
		try
		{
			if (exact)
			{
				NumberValue negOne = new NumberValue("-1", "1");
				retval = negOne.multiply(this);
				retval.reduce();
			}
			else
				retval = new NumberValue(-1.0*approx());
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: negate method numeric exception.");
		}

		return retval;
	}

	public NumberValue invert()
	{
		NumberValue retval = null;
		
		try
		{
			if (exact)
			{
				retval = new NumberValue(den, num);
				retval.reduce();
				if (retval.getDen().compareTo(BigInteger.ZERO) == 0) 
					throw new ArithmeticException("Division by zero.");
			}
			else
				retval = new NumberValue(1.0/approx());
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: invert method numeric exception.");
		}

		return retval;
	}

	public NumberValue pow(int p)
	{
		NumberValue retval = new NumberValue("1", "1");
		NumberValue base = clone();

		if (isZero() && (p > 0))
			return new NumberValue("0", "1");
		else if (isZero() && (p <= 0))
			throw new ArithmeticException("Division by zero.");
		
		if (isOne())
			return new NumberValue("1", "1");
		
		if (p < 0)
		{
			base.invert();
			p = -p;
		}
	
		try
		{
			if (exact)
			{
				for (int i = 0; i < p; i++)
					retval = retval.multiply(base); 
			}
			else
				retval = new NumberValue(Math.pow(approx(), (double)p));
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: pow(int) method numeric exception.");
		}

		return retval;
	}
	
	public NumberValue pow(double p)
	{
		try
		{
			return new NumberValue(Math.pow(approx(), p));
		} catch (Exception e)
		{
			throw new ArithmeticException("NumberValue: pow(double) method numeric exception.");
		}
	}
		
	public double approx()
	{
		double retval;
		
		if (exact)
			retval = num.doubleValue()/den.doubleValue();
		else
			retval = val;
		
		return retval;
	}
	
	public void reduce()
	{
		if (exact)
		{
			BigInteger gcdval = num.gcd(den);
			num = num.divide(gcdval);
			den = den.divide(gcdval);
			if (den.compareTo(BigInteger.ZERO) == -1)
			{
				BigInteger negOne = new BigInteger("-1");
				num = num.multiply(negOne);
				den = den.multiply(negOne);
			}
		}
	}

	public boolean isNegative()
	{
		return !isPos();
	}

	public boolean isPositive()
	{
		return isPos();
	}
	
	public boolean lt(NumberValue n)
	{
		return (approx() < n.approx());
	}	

	public boolean le(NumberValue n)
	{
		return ((approx() < n.approx()) || equals(n));
	}	

	public boolean gt(NumberValue n)
	{
		return (approx() < n.approx());
	}	

	public boolean ge(NumberValue n)
	{
		return ((approx() > n.approx()) || equals(n));
	}	

	public boolean equals(NumberValue n)
	{
		if (exact && n.isExact())
		{
			reduce();
			n.reduce();
			if ((num.compareTo(n.getNum()) == 0) && (den.compareTo(n.getDen()) == 0))
				return true;
			else
				return false;
		}
		else
			return (approx() == n.approx());
	}	
	
	public BigInteger getNum()
	{
		return num;
	}

	public BigInteger getDen()
	{
		return den;	
	}

	public synchronized NumberValue clone()
	{
		if (exact)
			return new NumberValue(num, den);

		return new NumberValue(val);
	}	

	public String toString()
	{
		reduce();
		String retstr = "";
		if (exact)
		{
			if (den.compareTo(BigInteger.ONE) == 0)
				retstr = num.toString();
			else
				retstr = num.toString() + "/" + den.toString();
		}
		else
			retstr = "" + val;
			
		return retstr;
	}

	public String toLaTeXString()
	{
		reduce();
		String retstr = "";
		if (exact)
		{
			if (den.compareTo(BigInteger.ONE) == 0)
				retstr = num.toString();
			else
			{
				if (isPos())
					retstr = " \\frac{" + num.toString() + "}{" + den.toString() + "} ";
				else
					retstr = " - \\frac{" + num.negate().toString() + "}{" + den.toString() + "} ";
			}
		}
		else
			retstr = "" + val;
			
		return retstr;
	}

	public static NumberValue factorial(long n)
	{
		if ((n == 0) || (n == 1)) return NumberValue.makeOne();
		return (new NumberValue(n, 1)).multiply(factorial(n-1));
	}

}